-
Notifications
You must be signed in to change notification settings - Fork 245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[JENKINS-70101] Revive ability to snapshot() the CertificateCredentials so they can be used on remote agents #391
base: master
Are you sure you want to change the base?
Conversation
…des [JENKINS-70101]
…tials() et al to standalone cases
…able*() to make sure it is right
…tial" to differentiate from expected other credential types
…equest() in the same pipeline (using same bytes)
Problem exposed in build logs, e.g. in https://github.com/jenkinsci/credentials-plugin/pull/391/checks?check_run_id=9617279731 :
Will post the fix commits now :) |
…r getUploadedKeystore()
…dedKeystore" be used to serialize
…d, must use Secret not SecretBytes
…urce() depend on Channel.current() == null
Tests fixed. Now posting the final touch, to make them quieter. This passes - and good to merge (on my side) :) |
@jenkinsci/core-security-review Should probably review this. |
Yep. In particular, I wonder if it is okay to leave it like this (possibly storing keystore data "plaintext" in a I have a further effort to force conversion from |
@jenkinsci/core-security-review Gentle bump :) |
Hey @jimklimov, we've noticed the issue. We'll try to review it if/when time permits. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From a security standpoint, I don't think there is any particular issue with taking snapshots of CertificateCredentialsImpl
, so you can consider this approved by the Jenkins security team.
My best guess as to what happened is that the original author of 40d0b5c based their fix on a similar fix in ssh-credentials
(jenkinsci/ssh-credentials-plugin@18b3121). In that plugin, the security fix eliminated the only remaining key source that required snapshot support so all related code was (correctly) deleted. In this plugin, the situation appears more complicated.
I only looked at the issue and the fix briefly, but I am a little confused about the behavior before the SECURITY-1322 fix though. Your PR fixes the issue by using Secret
for the data instead of SecretBytes
. This makes me think that the relevant functionality has been broken ever since #64, which IIUC changed CredentialsSnapshotTakerImpl
from using Secret
to SecretBytes
, and appears to use SecretBytes
directly in the credentials it returns from snapshot
. Any idea if that is correct?
If so, I would be inclined to effectively revert #64 and switch back to Secret
in all cases so that we do not need to (re)introduce CertificateCredentialsSnapshotTaker
and all of the supporting code. If not, then I don't understand why things would be broken before this PR (but there is a good chance I am misunderstanding something). Also, why doesn't FileCredentialsImpl, which also uses SecretBytes
, need a custom CredentialsSnapshotTaker
? Maybe it does, but it's not a common enough issue for anyone to have tried to fix it yet (or maybe I just didn't notice it in a quick search)?
The docs here makes me think that maybe this code in http_request
should instead be modified somehow, but I'm not sure what the preferred alternative is. Resolve the keystore and password directly there and just send them as Secret
s instead of the entire Credentials
object?
As a consequence, serialized copies of the key store used the same SecretBytes as on the Jenkins Controller, but being on a different JVM in the remote agent case, they used a different instance of the static KEY field with its own secret field.
This is a bit of a side note, what makes Secret
special? Is there something in remoting that treats it differently? Secret.KEY
is comparable to SecretBytes.KEY
, so I don't really understand what the difference would be here.
Thanks for the diligently annotated response :) Regarding FileCredentialsImpl - I did not use it so maybe the issue is there, just unnoticed. Probably multi-agent tests like those I proposed here can be used to check if it works or is similarly broken.
Regarding "correctness" of the fix: it seems that
According to my tracing, the referenced code of http_request runs on the controller, as the class is initialized for the step, and so the original credential and its snapshot are readable there. By the time it gets to the actual httpExecute and specifically As I mentioned above, in another branch (and draft PR #393 for review convenience) I have additional tracing and a commit 537d44d to convert from I have no strong opinion about reverting the older change (from Secret to SecretBytes), but still would be reluctant to do so:
Finally, regarding the idea to effectively redefine Maybe a similar idea is viable - to store the name of credential and request it (from agent if needed) as that doc suggests. But in case of long-lived HttpRequestExecution objects (freestyle?) this leaves a time gap for credential to change or disappear. A snapshot protects from that. So it likely makes sense to log an RFE issue in http-request-plugin to rearchitect this part, but I was not on a crusade to make all of the world better - got dayjob tasks to tend to, so have to be satisfied by getting it working and leave refactoring to others :) But since the root-cause was broken snapshot() ability here, well... it is a bug that should be fixed anyway. Work with that plugin just led me to the discovery :) |
Ah, yeah I did not realize that |
At least that's an option to keep in mind. If it bites someone else. This use-case issue took 3? years to be noticed intently enough... I am not certain about reasoning behind |
This comment and the reply make me think that the main goals were code deduplication and to avoid the need to Base64-encode raw |
Ok, I'll trust your judgement on this then - you're the security team and know more context overall :) Now, what about this-here PR? To me and my tasks it brings "immediate relief" for an issue at hand; you seem to be in favor of deeper design changes... Can this one be merged as proposed and the rest iterated separately? :) |
From my perspective, this general goal of this PR and the approach seem fine. My personal opinion is that #391 (comment) might be simpler and help us avoid related issues in the long run, but that's just my opinion. I am not an active maintainer of this plugin, so I'll request a review and see if anyone who is responsible for it wants to review it (sorry, I should have done this after my initial review). I have not reviewed the implementation carefully, but generally speaking the tests look more complicated than I would expect. I don't think we want to take on dependencies on In case you did not already know, you can always download a build of the plugin with your PR from the "Incremental" GitHub check to use while you wait for a maintainer. The latest build as of the time of writing this comment can be found here: https://repo.jenkins-ci.org/incrementals/org/jenkins-ci/plugins/credentials/1235.v3213f694ca_1c/. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a few comments related to the tests. I only took a quick look, so I might have misunderstood some things.
return agentUsable; | ||
} | ||
|
||
Boolean setupAgent() throws IOException, InterruptedException, OutOfMemoryError { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use JenkinsRule.createOnlineSlave
and delete this method, or is there a significant difference that I missed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Primarily, was not aware about it. Probably none of the sources I grepped into (nor IDEA highlighting) hit the keywords I used, which alas remains the best method of random ad-hoc learning so far :\
So even if I did reinvent a wheel, looks like mine is quite round at least :)
SystemCredentialsProvider.getInstance().save(); | ||
} | ||
|
||
String cpsScriptCredentialTestImports() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you have to import this stuff, you should write a @TestExtension
Step
instead (or just a MasterToSlaveCallable
, since I don't think Pipeline is required to test this issue).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the hint, now that I know the keywords, I'll try to find examples for inspiration :)
"node {\n" + | ||
cpsScriptCertCredentialTestScriptedPipeline("CONTROLLER NODE") + | ||
"}\n"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Groovy code in a Pipeline always executes on the controller. Wrapping everything in node
does not actually change the behavior related to the issue you are seeing - this test, testCertKeyStoreReadableOnController
, and testCertKeyStoreReadableOnNodeRemote
should all have the same behavior as far as the credential is concerned regardless of your changes (unless I missed something in the Pipeline script that actually makes use of the agent). To actually make use of the FilePath
or Launcher
provided by the node
you would need to write a Step
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my case it was more to rule out that nothing pops up with plain code and an unnamed node. It certainly did differ (original problem) for "named node" (or agent with label in declarative), with part of the plugin handling running on the remote agent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but in these three test cases, the nodes are not actually used by the credential-related code in the Pipelines. Regardless, I would replace your new tests with the minimal test from 4ee16f3.
Anyway, since I am trying to be helpful and not just nitpicky (😄), take a look at master...dwnusbaum:credentials-plugin:JENKINS-70101 to see what a fix that changes That said, my test in that branch is minimal and reproduces the |
As detailed in https://issues.jenkins.io/browse/JENKINS-70101 I've hit a problem using certificate-based user authentication in http-request-plugin (noticed as part of my work on https://issues.jenkins.io/browse/JENKINS-70000 in jenkinsci/http-request-plugin#120 but also reproduced with latest 1.16 release of that plugin).
After a week of reproductions and tracing across different plugins, the root of the problem crystallized as the use of
SecretBytes
inUploadedKeyStoreSource
and effective lack of specialsnapshot()
support. As a consequence, serialized copies of the key store used the sameSecretBytes
as on the Jenkins Controller, but being on a different JVM in the remote agent case, they used a different instance of the staticKEY
field with its ownsecret
field. Although theSecretBytes
are diligently copied (took a lot of effort to confirm that as seen in sibling branch https://github.com/jimklimov/credentials-plugin/tree/JENKINS-70101-trace), they are unreadable on the remote agent.During investigation I found that credentials-plugin did have an implementation for the Certificate Credentials Snapshot Taker, but it was removed by 40d0b5c as part of deprecation of
FileOnMasterKeyStoreSource
subclass. Also an earlier history of the plugin involved theSecret
class which effectively stores a base64 string and only encodes/decodes it on demand -- this was superseded bySecretBytes
but some handling for conversion from older versions remained. This code proved to be a good starting point for fixing the problem, although not without some further work:isSnapshotSource
(forcedtrue
forUploadedKeyStoreSource
subclass) and just returned the original credential if so;SecretBytes
for the copied key store and suffered the same fate - not usable on agent.This PR adds tests (failing at first to confirm the problem), makes use of older codebases and new verified fixes:
UploadedKeyStoreSource.isSnapshotSource()
depends onChannel.current() == null
(so for remoting-related snapshots it isfalse
and shortcuts toreturn this
are not taken)CertificateCredentialsSnapshotTaker
was revived as a separate source file and standalone class, similar to existingUsernamePasswordCredentialsSnapshotTaker
UploadedKeyStoreSource
was modified to handle again aSecret uploadedKeyStore
field (and provide data from it ifSecretBytes uploadedKeyStoreBytes
are currentlynull
). As part of that,transient
andfinal
modifiers on these fields had to go.CertificateCredentialsSnapshotTaker
was modified to create the newUploadedKeyStoreSource
instance withSecret
version rather thanSecretBytes
(asCredentialsSnapshotTaker
docs stipulate, "all the details are captured within the credential")Thanks to @mawinter69 and @slide for bright ideas and pointers, and general sympathy as I woed on the chat :)
Separately note that this PR adds tests using a separate JVM for the build agent to reproduce the problem. This code may be worth exporting into some
JenkinsAgentRule
if someone is up for it.NOTE: I'll post commits in several phases, so CI has its chance to fail with the tests and show the original problem in logs.